# qq音乐爬虫


#歌曲文件存储在本地自己指定的路径下面
#歌曲详情存储在mysql数据库中，若要存储，只需在获取数据库连接的函数connect函数的代码中改变相应的连接信息即可
#如果只是下载歌曲文件，不在数据库中存储歌曲信息的话，不用执行主菜单中的【2】，防止代码报错


#导入的库
import requests
from urllib.request import *
import json
import mysql.connector
import os.path


#程序主菜单
def menu():
    print("-----------主菜单-----------")
    print("[1]->搜索音乐")
    print("[2]->信息查询") 
    print("[3]->退出")
    print("---------------------------")


#音乐信息爬取及下载菜单项
def serviceMenu():
    print("---------音乐服务---------")
    print('[1]->获取音乐信息')
    print('[2]->下载全部')
    print('[3]->单曲下载')
    print('[4]->返回上一级')
    print("---------------------------")


#数据库信息操作菜单项
def infoMenu():
    print("---------信息服务---------")
    print("[1]->查看全部已下载音乐信息")
    print("[2]->检索歌曲信息")
    print('[3]->删除指定歌曲')
    print('[4]->清空所有歌曲')
    print('[5]->返回上一级')
    print("---------------------------")


#获取除歌曲地址之外的其它信息函数（该函数接收一个输入的歌曲名或歌手名）
def getJsonSingerData(nameInfo):
    url = "https://c.y.qq.com/soso/fcgi-bin/client_search_cp?&t=0&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=1&n=20&w={}".format(nameInfo)

    html = requests.get(url)

    html.encoding = "utf-8"

    htmlText = html.text

    jsonData = json.loads(htmlText.strip("callback()[]"))

    jsonSingerData = jsonData["data"]["song"]["list"]

    return jsonSingerData


#创建一个歌曲类
class MusicClass:
    def __init__(self,meiTi_ID,music_ID,music_Name,singer,albumNames,music_URL=None):
        self.__meiTi_ID = meiTi_ID #歌曲的媒体ID
        self.__music_ID = music_ID #歌曲的ID
        self.__music_URL = music_URL #歌曲的地址
        self.__music_Name = music_Name #歌曲的名称
        self.__singer = singer #歌手
        self.__albumNames = albumNames #歌手专辑
        self.__local_music_path = None #歌曲在本地的存储路径

    #为其中外部要用到的私有属性提供get和set方法
    def getMeiTi_ID(self):
        return self.__meiTi_ID

    def getMusic_ID(self):
        return self.__music_ID

    def getMusic_Name(self):
        return self.__music_Name

    def getSinger(self):
        return self.__singer

    def getMusicPath(self):
        return self.__local_music_path

    def getAlbumNames(self):
        return self.__albumNames

    def setMusic_URL(self,music_url):
        self.__music_URL = music_url

    #提供显示歌曲详情的方法
    def showMusicInfo(self,index="*"):
        print("{}: {}-{} {}".format(index,self.__music_Name,self.__singer,self.__albumNames))

    #提供下载歌曲的类方法(根据传入的歌曲实例和保存路径下载歌曲)
    @classmethod
    def musicDownLoad(cls,self,downLoadUrl):
        try:
            music_path = "{}\\{}-{}.m4a".format(downLoadUrl,self.__music_Name,self.__singer)
            self.showMusicInfo()
            print("正在下载中...")
            urlretrieve(self.__music_URL,music_path)
            self.__local_music_path = music_path #给实例中的__local_music_path属性赋值
            print("下载完成。\n")
        except:
            print("下载失败！\n")

    #遍历并输出所有歌曲信息
    @classmethod
    def printAllMusic(self,musicList):
        for index in range(len(musicList)):
            musicList[index].showMusicInfo(index+1)


#给MusicClass类的实例中歌曲的地址赋值函数（该函数接收一个存储歌曲实例的列表）
def setClassMusicUrl(musicInfo):
    url1 = 'https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg?&jsonpCallback=MusicJsonCallback&cid=205361747&songmid={}&filename=C400{}.m4a&guid=6612300644'

    url2 = 'http://dl.stream.qqmusic.qq.com/C400{}.m4a?vkey={}&guid=6612300644&uin=0&fromtag=66'

    for m in musicInfo:
        html2 = requests.get(url1.format(m.getMusic_ID(),m.getMeiTi_ID()))

        html2.encoding = "utf-8"

        htmlText2 = html2.text

        jsonData2 = json.loads(htmlText2)

        vkey = jsonData2["data"]["items"][0]["vkey"]

        m.setMusic_URL(url2.format(m.getMeiTi_ID(),vkey))


#获取数据库连接
def connect():
    connection=mysql.connector.connect(host="localhost",user="root",password="",port="3306",charset="utf8")
    return connection


#关闭数据库游标对象和数据库连接对象
def closeAll(connection,cursor):
    cursor.close()
    connection.close()


#创建存储歌曲信息的数据库和数据表（参数musicInfo为存储歌曲类实例的列表）
def CreateMusicDatabase():
    myConnect=connect()
    myCursor=myConnect.cursor()
    # 创建数据库
    myCursor.execute("create database if not exists myMusicInfo")
    
    # 选择要操作的数据库
    myCursor.execute("use myMusicInfo")

    # 创建要存储歌曲信息的数据库表
    createTableSql = '''create table if not exists musicInfo(
                           id int primary key auto_increment,music_name varchar(20) not null,
                           singer_name varchar(20),music_url varchar(200),music_albumNames varchar(50)
                           )charset=utf8'''

    myCursor.execute(createTableSql)
    return (myConnect,myCursor)


#插入歌曲信息（歌曲名，歌手名，本地歌曲的存储路径，歌曲专辑名）到mysql数据库中（接收参数的参数musicInfo为存储歌曲实例的列表）
def insertMusicInfo(musicInfo):
    myConnect,myCursor=CreateMusicDatabase()
    inserSql = "insert into musicInfo(music_name,singer_name,music_url,music_albumNames) values (%s,%s,%s,%s)"

    insertInfo = [] #用来存储元组，其中每个元组存储了一条插入数据库的歌曲信息

    for m in musicInfo:
        t = (m.getMusic_Name(),m.getSinger(),m.getMusicPath(),m.getAlbumNames())
        insertInfo.append(t)

    myCursor.executemany(inserSql,insertInfo)

    myConnect.commit()

    closeAll(myConnect,myCursor)


#获取音乐信息
def getMusicInformation():

    nameInfo = input("请输入歌手或歌名：")

    jsonSingerData = getJsonSingerData(nameInfo) #获取除歌曲地址之外的所有歌曲信息

    musicInfo = [] #存储歌曲类的实例

    #对获取到的歌曲信息进行解析，把每个歌曲解析出来的信息存储在MusicClass的实例中
    for data in jsonSingerData:
        try:
            music = MusicClass(data["media_mid"],data["songmid"],data["songname"],data['singer'][0]['name'],data["albumname"])
            musicInfo.append(music)
        except:
            print("信息解析出现错误！\n")

    setClassMusicUrl(musicInfo) #为musicInfo列表中的每个实例的歌曲地址赋值
    return musicInfo


#下载歌曲
def downLoadMusic(musicInfo,dataList,index=-1):
    downLoadUrl = input("请输入要存储歌曲的本地路径：")
    #判断此路径是否存在
    if not os.path.exists(downLoadUrl):
        #创建此路径
        os.path.makedirs(downLoadUrl)
        #如果创建失败,则终止
        if not os.path.exists(downLoadUrl):
            return
    
    #如果index==-1则下载所有音乐
    if index==-1:
        for mu in musicInfo:
            MusicClass.musicDownLoad(mu,downLoadUrl)
        #清空原有的元素
        dataList.clear()
        #将musicInfo集合的元素全部添加到dataList集合中
        dataList.extend(musicInfo)
    
    #单曲下载
    elif index>-1 and index<len(musicInfo):
        MusicClass.musicDownLoad(musicInfo[index],downLoadUrl)
        if not musicInfo[index] in dataList:
            dataList.append(musicInfo[index])
    else:
        print("序号不正确\n")
        return
    print("歌曲下载完成\n")


#将已下载的歌曲存储到mysql数据库中（参数dataList存储的是已下载歌曲的实例）
def storeMusicInfo(dataList):
    info = input("是否存储歌曲信息到数据库中(Y/N):")
    if info == "Y":
        # 把歌曲信息存入到数据库中（mysql）
        # 创建存储信息的数据库和数据表,并进行存储
        insertMusicInfo(dataList)
        print("数据存储完成。\n")


#从数据库中查询所有歌曲信息
def queryAll():
    myConnect,myCursor=CreateMusicDatabase()
    myCursor.execute("use mymusicinfo")
    sql="select * from musicinfo"
    myCursor.execute(sql)
    result=myCursor.fetchall()
    if len(result)==0:
        print("没有任何数据！\n")
    else:
        for item in result:
            print("{} , {} , {} , {} , {}".format(item[0],item[1],item[2],item[4],item[3]))
    closeAll(myConnect,myCursor)


#在数据库中查询指定歌曲或歌手信息
def queryByName():
    name=input("请输入歌手或歌曲名：")
    sql="select * from musicinfo where music_name=%s or singer_name=%s"
    myConnect,myCursor=CreateMusicDatabase()
    myCursor.execute("use mymusicinfo")
    myCursor.execute(sql,(name,name))
    result=myCursor.fetchall()
    if len(result)==0:
        print("没有任何数据！\n")
    else:
        for item in result:
            print("{} , {} , {} , {} , {}".format(item[0],item[1],item[2],item[4],item[3]))
    closeAll(myConnect,myCursor)


#在数据库中删除指定歌曲或歌手信息
def delete(isDeleteAll):
    myConnect,myCursor=CreateMusicDatabase()
    myCursor.execute("use mymusicinfo")
    #全部删除
    if isDeleteAll:
        sql="delete from musicinfo"
        myCursor.execute(sql)
    #单个删除
    else:
        sql="delete from musicinfo where music_name=%s or singer_name=%s"
        name=input("请输入歌手或歌曲名：")
        myCursor.execute(sql,(name,name))
    myConnect.commit()
    print("*删除成功*\n")
    closeAll(myConnect,myCursor)


#主程序开始的函数
def start():
    dataList=[]#存储下载的歌曲信息(列表集合)
    while True:
        menu()#显示主菜单
        cmd=input("请输入菜单序号：")
        if '1'==cmd:
            musicList=getMusicInformation()#获取音乐信息列表集合
            while True:
                serviceMenu()#显示第一级的子菜单
                cmd=input("请输入菜单序号：")
                if '1'==cmd:
                   MusicClass.printAllMusic(musicList)#输出所有搜索到的音乐信息
                elif '2'==cmd:
                    downLoadMusic(musicList,dataList)#下载所有音乐
                elif '3'==cmd:
                    index=input("请输入序号：")#下载指定编号的音乐文件
                    if index.isdigit()==True:
                        downLoadMusic(musicList,dataList,int(index)-1)#开始下载
                    else:
                        print("序号必须是数值\n")
                else:
                    storeMusicInfo(dataList)#存储歌曲信息到数据库中
                    break #返回到上一级菜单
        elif '2'==cmd:
            while True:
                infoMenu()#显示第二级子菜单
                cmd=input("请输入菜单序号：")
                if '1'==cmd:
                    queryAll()#从数据库表中查询已下载音乐的信息
                elif '2'==cmd:
                    queryByName()#查询指定的歌曲或歌手的音乐信息
                elif '3'==cmd:
                    delete(False)#从数据库中单个删除歌曲信息
                elif '4'==cmd:
                    delete(True)#从数据库中清空所有歌曲信息
                else:
                    break #返回到上一级菜单
        else:
            break #程序结束


#主程序开始
start()